package czopyk.model.persistence;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import model.Time;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.log4j.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import com.espertech.esper.collection.Pair;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;

import czopyk.model.analysis.conditions.Condition;
import czopyk.model.analysis.conditions.Condition.StatsType;
import czopyk.model.analysis.conditions.Condition.WindowType;
import czopyk.model.analysis.conditions.LoadCondition;
import czopyk.model.analysis.conditions.ResponseTimeCondition;
import czopyk.model.analysis.conditions.ResponseTimeStatCondition;
import czopyk.model.cost.DistributionId;
import czopyk.model.persistence.actions.Action;
import czopyk.model.persistence.actions.ChangeCoresAction;
import czopyk.model.persistence.actions.ChangeDistributionAction;
import czopyk.model.persistence.actions.ChangeHitRateAction;
import czopyk.model.persistence.actions.ChangeUsersCountAction;
import czopyk.model.persistence.actions.ChangeVarAction;
import czopyk.model.persistence.actions.PrintAction;
import czopyk.model.persistence.actions.StopAction;
import czopyk.model.persistence.actions.VisualizeDistributionAction;
import czopyk.model.persistence.actions.VisualizeXYAction;
import czopyk.model.request.LoadClass;
import czopyk.model.structure.Modulator;

public class Controller implements ContentHandler, ValueExtractor {

	private Map<String, BigDecimal> variables;

	private final Logger lgr = Logger.getLogger("Controller");

	public Controller() {
		super();
	}

	public void load(String fileName) throws Exception {
		XMLReader parser = null;
		parser = XMLReaderFactory.createXMLReader();
		parser.setContentHandler(this);
		parser.parse(fileName);
	}

	private List<Action> actions = new LinkedList<Action>();

	String stopOffset, stopOffsetType, stopInterval, stopIntervalType, actionVar;

	private void clean() {
		// nie clear, zeby dzialaly stare warunki
		variables = new HashMap<String, BigDecimal>();
		distributions = new HashMap<Long, List<Double>>();
		idToVar = HashBiMap.create();
	}

	public void setDocumentLocator(Locator locator) {
	}

	public void startDocument() throws SAXException {
		clean();
	}

	// listy z rozkladami
	private Map<Long, List<Double>> distributions = new HashMap<Long, List<Double>>();

	// strumienie - nazwa i id
	private BiMap<Long, String> idToVar = HashBiMap.create();

	private Double loadReportingInterval;
	private Integer precision;

	public void endDocument() throws SAXException {

		czopyk.model.factories.Locator.getAnalyzer().addObserver(new Observer() {

			// obsługa akcji z właściwą częstotliwością
			final double interval = Double.valueOf(stopInterval);
			final boolean intervalClock = "clock".equalsIgnoreCase(stopIntervalType);
			double lastActionInvokeClock = 0.0;
			int eventToInvokeAction = (int) Math.round(interval);

			// obsługa offsetu
			final boolean offsetTypeClock = "clock".equalsIgnoreCase(stopOffsetType);
			final double offset = Double.valueOf(stopOffset);
			int registered = 0;

			private boolean offsetReached = false;
			
			private void offsetUpdate() {
				if (offsetReached) {
					return;
				}
				// obsługa offsetu
				if (offsetTypeClock) {
					if (Time.currentTime() < offset) {
						return;
					}
				} else {
					if (registered < offset) {
						return;
					}
				}
				offsetReached = true;
			}
			
			public void update(Observable o, Object arg) {
				try {
					if (arg instanceof Pair) {
						@SuppressWarnings("unchecked")
						Pair<Long, Double> p = (Pair<Long, Double>) arg;

						long id = p.getFirst();
						double val = p.getSecond();

						if (idToVar.keySet().contains(id)) {

							// rodzaj zdarzen uruchamia petle action

							Long actionVarId = idToVar.inverse().get(actionVar);
							if (actionVar != null && actionVarId == null) {
								throw new IllegalStateException("Variable '" + actionVar
										+ "' not set.");
							}

							if (actionVar == null || id == actionVarId) {
								// zliczenie
								++registered;
								offsetUpdate();
							}

							if (offsetReached) {
								// wstawienie wartosci do mapy
								variables.put(idToVar.get(id), BigDecimal.valueOf(val));
								// lgr.info(idToVar.get(id) + " " + val);

								// dodanie wartosci do tablicy
								if (distributions.keySet().contains(id)) {
									List<Double> distribution = distributions.get(id);
									distribution.add(val);
								}
							}
							
							if (offsetReached && (actionVar == null || id == actionVarId)) {
								
								if (intervalClock) {
									if (Time.currentTime() - lastActionInvokeClock < interval) {
										// nieprzekroczenie interwału czasu
										return;
									}
									lastActionInvokeClock = Time.currentTime();
									// akcja
								} else {
									if (eventToInvokeAction-- > 0) {
										return;
									}
									eventToInvokeAction = (int) Math.round(interval);
									// akcja
								}

								for (Action a : actions) {
									a.perform();
								}

								cleanDistributions();
							}
						}
					}
				} catch (Exception e) {
					if (StringUtils.isNotBlank(e.getMessage())) {
						lgr.error(e.getMessage(), e);
					} else {
						e.printStackTrace();
					}
					Time.stop();
				}
			}

			private void cleanDistributions() {
				for (Long key : distributions.keySet()) {
					distributions.put(key, new LinkedList<Double>());
				}
			}
		});

		czopyk.model.factories.Locator.getAnalyzer().setPrecision(precision);
		czopyk.model.factories.Locator.getSimulator().setLoadReportInterval(
				this.getLoadReportingInterval());
	}

	public void startPrefixMapping(String prefix, String uri) throws SAXException {
	}

	public void endPrefixMapping(String prefix) throws SAXException {
	}

	public void startElement(String uri, String localName, String qName,
			final Attributes atts) throws SAXException {
		Modulator mod = czopyk.model.factories.Locator.getModulator();

		if ("condition".equalsIgnoreCase(localName)) {
			// zbudowanie warunku
			Condition condition = buildCondition(atts);

			// rejestracja
			long id = czopyk.model.factories.Locator.getAnalyzer()
					.getAnalysisStreamOfCondition(condition);

			// wpakowanie do mapy jako vara
			idToVar.put(id, atts.getValue("var"));

		} else if ("actionOn".equalsIgnoreCase(localName)) {
			stopInterval = atts.getValue("interval");
			stopIntervalType = atts.getValue("intervalType");
			stopOffset = atts.getValue("offset");
			stopOffsetType = atts.getValue("offsetType");
			actionVar = atts.getValue("var");
		} else if ("var".equalsIgnoreCase(localName)) {
			variables.put(atts.getValue("id"), BigDecimal.valueOf(Double
					.valueOf(getValue(atts.getValue("initValue")))));
		} else if ("visualizeDistribution".equals(localName)) {
			actions.add(new VisualizeDistributionAction(atts.getValue("var"), atts
					.getValue("label"), atts.getValue("chart"), "pdf"
					.equalsIgnoreCase(atts.getValue("type")), idToVar, distributions, this));
		} else if ("print".equals(localName)) {
			actions.add(new PrintAction(atts.getValue("value"), lgr, this));
		} else if ("changeVar".equalsIgnoreCase(localName)) {
			actions.add(new ChangeVarAction(atts.getValue("id"), atts
					.getValue("action"), atts.getValue("value"), variables, this));

			// zmiany parametrów
		} else if ("changeDistribution".equalsIgnoreCase(localName)) {
			actions.add(new ChangeDistributionAction(new DistributionId(atts
					.getValue("id")), atts.getValue("params"), atts.getValue("offset"),
					mod, this));
		} else if ("setCores".equalsIgnoreCase(localName)) {
			actions.add(new ChangeCoresAction(atts.getValue("centerName"), atts
					.getValue("value"), mod, this));
		} else if ("setHitRate".equalsIgnoreCase(localName)) {
			actions.add(new ChangeHitRateAction(atts.getValue("centerName"),
					atts.getValue("value"), mod, this));
		} else if ("setUsersCount".equalsIgnoreCase(localName)) {
			actions.add(new ChangeUsersCountAction(atts.getValue("value"), mod, this));
		} 
		/*else if ("setInterferenceFactor".equalsIgnoreCase(localName)) {
			actions.add(new SetInterferenceAction(atts.getValue("centerName"),
					atts.getValue("value"), mod, this));
		}*/

		else if ("stop".equalsIgnoreCase(localName)) {
			actions.add(new StopAction(atts.getValue("var"), atts.getValue("value"),
					atts.getValue("condition"), this));
		} else if ("loadReportingInterval".equalsIgnoreCase(localName)) {
			String v = atts.getValue("value");
			if (!NumberUtils.isNumber(v)) {
				throw new IllegalArgumentException(
						"LoadReportingInterval value must be numeric.");
			}
			loadReportingInterval = Double.valueOf(v);
		} else if ("precision".equalsIgnoreCase(localName)) {
			String v = atts.getValue("value");
			if (!StringUtils.isNumeric(v)) {
				throw new IllegalArgumentException("Precision value must be numeric.");
			}
			precision = Integer.valueOf(v);
		} else if ("visualizeXY".equalsIgnoreCase(localName)) {
			actions.add(new VisualizeXYAction(atts.getValue("varX"), atts
					.getValue("varY"), atts.getValue("label"), atts.getValue("chart"), this));
		}
	}

	private Condition buildCondition(Attributes a) {
		Condition c = null;

		double windowSize = Double.valueOf(a.getValue("windowSize"));
		WindowType windowType = WindowType.valueOf(a.getValue("windowType")
				.toUpperCase());

		String b = a.getValue("boundary");
		double boundary = StringUtils.isEmpty(b) ? 1.0 : Double.valueOf(b);

		LoadClass lc = czopyk.model.factories.Locator
				.getRequestFactory()
				.getLoadClass(a.getValue("loadClassType"), a.getValue("loadClassClazz"));

		b = a.getValue("statsType");
		StatsType st;
		try {
			st = StringUtils.isEmpty(b) ? null : StatsType.valueOf(b.toUpperCase());
		} catch (IllegalArgumentException e) {
			throw new IllegalArgumentException("No stats type named '" + b
					+ "'. Available: " + Arrays.toString(StatsType.values()));
		}

		String centerName = a.getValue("centerName");

		if ("responseTime".equalsIgnoreCase(a.getValue("type"))) {
			c = new ResponseTimeCondition(windowSize, windowType, boundary, lc);
		} else if ("responseTimeStat".equalsIgnoreCase(a.getValue("type"))) {
			if (st == null) {
				throw new IllegalArgumentException(
						"Condition 'responseTimeStat' requires property 'statsType' "
								+ "{AVERAGE, VARIANCE, STDDEV, DATAPOINTS, THROUGHTPUT}.");
			}
			c = new ResponseTimeStatCondition(windowSize, windowType, st, lc);
		} else if ("load".equalsIgnoreCase(a.getValue("type"))) {
			if (centerName == null) {
				throw new IllegalArgumentException(
						"No 'centerName' property at condition of type load.");
			}
			c = new LoadCondition(windowSize, windowType, centerName);
		} else {
			throw new IllegalArgumentException("Unknown condition type: "
					+ a.getValue("type"));
		}
		return c;
	}

	public void endElement(String uri, String localName, String qName)
			throws SAXException {

	}

	public void characters(char[] ch, int start, int length) throws SAXException {
	}

	public void ignorableWhitespace(char[] ch, int start, int length)
			throws SAXException {
	}

	public void processingInstruction(String target, String data)
			throws SAXException {
	}

	public void skippedEntity(String name) throws SAXException {
	}

	public static final String EOL = System.getProperty("line.separator");

	private static final String REGEX = "\\#\\{(\\w|\\s)+\\}";
	Pattern p = Pattern.compile(REGEX);

	public String getValue(String value) {
		Matcher m = p.matcher(value);
		String newValue = value;
		while (m.find()) {
			String varExp = value.substring(m.start(), m.end());
			String var = value.substring(m.start() + 2, m.end() - 1);

			BigDecimal db = getNumber(var);
			if (db != null) {
				newValue = newValue.replaceFirst("\\Q" + varExp + "\\E",
						db.doubleValue() + "");
			}
		}
		return newValue;
	}

	public BigDecimal getNumber(String value) {
		BigDecimal arg;
		if (NumberUtils.isNumber(value)) {
			arg = BigDecimal.valueOf(Double.valueOf(value));
		} else if ("time".equals(value)) {
			return BigDecimal.valueOf((double) Math.round(Time.currentTime()));
		} else if ("users".equals(value)) {
			return BigDecimal.valueOf(czopyk.model.factories.Locator
					.getLoadGenerator().getUsersCount());
		} else {
			arg = variables.get(value != null ? value : "");
		}
		return arg;
	}

	public Double getLoadReportingInterval() {
		return loadReportingInterval;
	}

	public Integer getPrecision() {
		return precision;
	}

}
